Hello, hustlers! In this tutorial, you'll learn step-by-step how to create a react multiple file upload component in a NextJS project.
Tutorial objectives#In this tutorial, you'll create an image upload component that can do the following:
Select multiple images from the user's computer,Preview images before uploading, Create a backend API endpoint for file uploadWrite the uploaded images to the local file system.Ultimately, to upload multiple files, I find that it's best to iterate through the selected files from an {\n return (\n\n );\n};\n\nexport default CustomFileSelector;\n"]}' ssr="" client="load" opts='{"name":"Code","value":true}' await-children="">// components/CustomFileSelector.tsximport classNames from "classnames";import React, { ComponentPropsWithRef } from "react";type Props = ComponentPropsWithRef;const CustomFileSelector = (props: Props) => { return ( );};export default CustomFileSelector;1234567891011121314151617181920212223242526
In line 2, You import the classnames package so that you can use it later when organizing the classes.
In line 5, You copy all the props of the HTMLInputElement by using the ComponentPropsWithRef utility type so that when you use the we can pass it props like onChange an value.
In line 10, the type="file" option will enable the input element to select files from the user's computer.
In line 11, the multiple property allows it to select multiple files as opposed to only one file.
In lines 12-20, you use multiple tailwind classes to style the component.
Now, let's modify the app/page.tsx file to use the CustomFileSelector component.
// app/page.tsximport CustomFileSelector from "@/components/CustomFileSelector";export default function Home() { return ();}12345678910The output should look like this:
Step 2 - Previewing the Selected Images#Creating the ComponentNext, let's create a component to preview the selected images and create the file components/ImagePreview.tsx.
// components/ImagePreview.tsximport React from "react";import Image from "next/image";type Props = { images: File[];};const ImagePreview = ({ images }: Props) => { return ( {images.map((image) => { const src = URL.createObjectURL(image); return ();})});};export default ImagePreview;1234567891011121314151617181920212223242526First of all, this is a display-only component that uses an array of File objects. In line 12, you use a grid layout to display the images. In lines 13-20, you iterate through the File array and get the URL by using URL.createObjectURL method. The URL.createObjectURL is a very convenient method to get file URLs from Files or Blobs. Then we just use the next/image component to render the images.
Wiring the components togetherNext, let's create a container Component that uses both of the components you just created so that it can listen to the onChange event from the CustomFileSelector comopnent and pass the selected files to the ImagePreview component.
Create the file components/FileUploadForm.tsx:
"use client"; // Make this component a client component// components/FileUploadForm.tsximport React, { useState } from "react";import CustomFileSelector from "./CustomFileSelector";import ImagePreview from "./ImagePreview";const FileUploadForm = () => { const [images, setImages] = useState([]); const handleFileSelected = (e: React.ChangeEvent) => {if (e.target.files) { //convert `FileList` to `File[]` const _files = Array.from(e.target.files); setImages(_files);} }; return ( );};export default FileUploadForm;123456789101112131415161718192021222324252627We use the "use client" directive to mark this component as a client component in NextJS. Otherwise, you won't be able to use hooks like useState.
On line 7, you create a images state to store the selected files using the useState hook. You use a generic parameter to indicate that this state is of File[] type. Otherwise, it will use the never[] type and result in a type error later when we use the setImages function.
On line 8, we create a handler handleFileSelected to listen for value changes in the CustomFileSelector component. First, you check if there are selected files using the e.target.files property which is a FileList object.
However, the FileList object is hard to work with so we transform it into a File[] using Array.from method. Then, you just set the state using the setImages function.
On line 22, we use the ImagePreview component to display the selected images.
Modify app/page.tsxNext, let's modify the /app/page.tsx file to use the FileUploadForm component.
// app/page.tsximport FileUploadForm from "@/components/FileUploadForm";export default function Home() { return ();}12345678910After this step, the output should be like the screenshot below once you select files from your computer:
Step 3 - Adding an Upload Button#Next, let's add an Upload or "submit" button to our form.
// components/FileUploadForm.tsx// ...const FileUploadForm = () => { // ... return (Upload );};export default FileUploadForm;12345678910111213141516171819202122232425In line 7, you use the flex layout with justify-between class to line up both CustomFileSelector and the submit button.
In lines 12-17, you use a regular button element with tailwind CSS styling.
Step 4 - Handling onSubmit Functionality#Next, modify the FileUploadForm component to handle the form submission:
// components/FileUploadForm.tsx"use client"; // Make this component a client componentimport React, { FormEvent, useState } from "react";import CustomFileSelector from "./CustomFileSelector";import ImagePreview from "./ImagePreview";import axios from "axios";import classNames from "classnames";const FileUploadForm = () => { const [images, setImages] = useState([]); const [uploading, setUploading] = useState(false); const handleFileSelected = (e: React.ChangeEvent) => {if (e.target.files) { //convert `FileList` to `File[]` const _files = Array.from(e.target.files); setImages(_files);} }; const handleSubmit = async (e: FormEvent) => {e.preventDefault();const formData = new FormData();images.forEach((image, i) => { formData.append(image.name, image);});setUploading(true);await axios.post("/api/upload", formData);setUploading(false); }; return (Upload );};export default FileUploadForm;12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455First, in line 5, you import axios package to later send a post request to the upload API endpoint that you'll create later.
In lines 20-30, you define an asynchandleSubmit callback to handle the file upload.
In line 21, the e.preventDefault() method is used so that the form won't send a POST request to the current URL/route.
In line 23, You create a new FormData object. The FormData object provides a way to easily construct a set of key/value pairs representing form fields and their values which can be easily sent using the axios.post() method.
In lines 24-26, You iterate through the selected images and use formData.append() method to set the key/value pairs.
In lines 27-29, You set the uploading state to true, and call the axios.post() method. The axios.post method accepts a URL as the first parameter and the data as the second parameter. After everything is uploaded, the uploading state is then set to false.
Step 5 - Implementing the Upload API#Currently, when you click the Upload button, it doesn't do anything yet because the /api/upload endpoint will return 404 status. In this step, you'll create the /api/upload endpoint to save the selected images in the local /public directory of our project.
In production apps, you should save the images in a file storage service like AWS S3 or MongoDB GridFS.
Create the file /app/api/upload/route.ts:
import fs from "fs";import { NextResponse } from "next/server";export async function POST(req: Request) { const formData = await req.formData(); const formDataEntryValues = Array.from(formData.values()); for (const formDataEntryValue of formDataEntryValues) {if (typeof formDataEntryValue === "object" && "arrayBuffer" in formDataEntryValue) { const file = formDataEntryValue as unknown as Blob; const buffer = Buffer.from(await file.arrayBuffer()); fs.writeFileSync(`public/${file.name}`, buffer);} } return NextResponse.json({ success: true });}123456789101112131415In line 4, we export an async function POST which is a convention in NextJS 13 route.ts files that indicates that the route will be able to handle POST requests.
It accepts a Request object for the first argument which extends the Web Request API. This gives you helpful methods like formData(), blob(), json(), arrayBuffer() to easily process the data. This is not the case when using the `/pages/api` request handlers.
In line 5, you can easily get the formData by using the req.formData() method. Then in line 6, you transform the formData values into an array of FormDataEntryValue objects. A FormDataEntryValue can either be of type string or Blob.
In line 7, we iterate through all the formDataEntryValues. At this point, the type for each formDataEntryValue is still unknown so you have to use a guard on line 8.
On line 9, you're now sure that the formDataEntryValue is of type Blob so we use "type assertion" using the as operator. Now, you can use the methods of the Blob object such as arrayBuffer().
On line 10, you use the file.arrayBuffer() method which returns a Promise object. Next, you transform it into a Buffer object by using the Buffer.from method. The Buffer.from method accepts either a String, Array, Buffer, ArrayBuffer and transforms it into a Buffer object.
On line 11, Since you already have the Buffer as data, you use fs.writeFileSync to write the image into the public directory. Note that in production apps, you should upload the file to a file storage service.
Finally, on line 14, we return a JSON response.
We were able to process the request without the help of external libraries and I find using this technique incredibly easy as opposed to the /pages/api directory.
That's basically it!
Full Code and Demo#You can check out the full code on Github: jmarioste/next-multiple-file-upload-tutorial.
For the demo, you can run the project locally by running the command:
npx create-next-app -e https://github.com/jmarioste/next-multiple-file-upload-tutorial file-upload-democd file-upload-demoyarn devConclusion#You learned how to create a full feature to upload multiple files using React, NextJS, and TailwindCSS. Using the NextJS 13 /app router makes processing file upload request incredibly easy and straightforward.
If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.
Credits: Image by John Lee from Pixabay